package com.wnxy.sddfp.system.service.impl;
import java.math.BigDecimal;
import java.security.Key;
import java.time.Duration;
import java.util.Map;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.RandomUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
import com.wnxy.sddfp.client.bank.TradeLogApi;
import com.wnxy.sddfp.common.constant.RedisConstant;
import com.wnxy.sddfp.common.constant.UserConstant;
import com.wnxy.sddfp.common.exception.Assert;
import com.wnxy.sddfp.common.exception.BusinessException;
import com.wnxy.sddfp.common.model.dto.bank.BankCardAddDTO;
import com.wnxy.sddfp.common.model.dto.bank.CardBindDTO;
import com.wnxy.sddfp.common.model.dto.bank.SignatureDTO;
import com.wnxy.sddfp.common.model.dto.user.LoginDto;
import com.wnxy.sddfp.common.model.dto.user.UserRegistDto;
import com.wnxy.sddfp.common.model.vo.user.LoginVo;
import com.wnxy.sddfp.common.model.vo.user.TokenVo;
import com.wnxy.sddfp.common.model.vo.user.UserInfoVo;
import com.wnxy.sddfp.common.result.Result;
import com.wnxy.sddfp.common.result.enums.impl.BusinessCode;
import com.wnxy.sddfp.common.result.enums.impl.ResultCode;
import com.wnxy.sddfp.common.util.JwtTemplate;
import com.wnxy.sddfp.common.util.TokenUtils;
import com.wnxy.sddfp.common.util.UniqueIDGenerator;
import com.wnxy.sddfp.system.entity.User;
import com.wnxy.sddfp.system.entity.UserCard;
import com.wnxy.sddfp.system.entity.UserDetail;
import com.wnxy.sddfp.system.mapper.UserMapper;
import com.wnxy.sddfp.system.service.IUserCardService;
import com.wnxy.sddfp.system.service.IUserDetailService;
import com.wnxy.sddfp.system.service.IUserService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.http.ResponseEntity;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;

/**
 * <p>
 * 用户表 服务实现类
 * </p>
 *
 * @author Jet
 * @since 2023-07-24
 */
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {

    @Autowired
    private IUserDetailService userDetailService;
    @Autowired
    private PasswordEncoder passwordEncoder;
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Autowired
    private JwtTemplate jwtTemplate;
    @Autowired
    private IUserCardService userCardService;
    @Autowired
    private RestTemplate restTemplate;
    @Autowired
    private TradeLogApi tradeLogApi;

    /**
     * 用户注册：
     * 1、根据手机号、密码、用户名进行注册；
     * 2、注册需要验证手机号是否重复；
     * 3、需要保存用户信息、用户详情信息；
     *
     * @Transactional
     * 1、事务注解：方法中所有的数据库操作会作为一个整体，要么同时成功；只要有一个失败就全部失败；
     * 2、rollbackFor 指定遇到指定的异常才回滚
     *      默认回滚的是RuntimeException运行时期异常； 不会回滚检查异常；
     *      如果要回滚所有异常，指定rollbackFor = Exception.class
     * 3、propagation 事务传播行为; 作用：对方法如何应用事务
     *      REQUIRED 默认值；当前运行方法必须有事务环境，如果没有，就创建事务; 如果有事务环境，就加入到当前事务
     *      SUPPORTS 修饰的方法可以在事务环境以及非事务环境中运行；适合：查询
     *      REQUIRES_NEW 表示当前方法运行必须有事务环境； 不管当前方法是否有事务环境，都创建一个全新的事务；
     */
    @Override
    @Transactional(rollbackFor = Exception.class,propagation = Propagation.REQUIRED)
    public void regist(UserRegistDto userRegistDto) {
        // 查询手机号码是否存在
        Long count = checkPhoneExists(userRegistDto.getPhone());

        // 判断： 如果查到数据，就抛出自定义异常，传入枚举对象（封装错误信息）
        Assert.error(count > 0, BusinessCode.PHONE_EXISTED);

        // 保存用户信息、用户详情信息；
        saveUserInfo(userRegistDto);
    }

    /**
     * 根据手机号查询手机号码是否存在
     */
    private Long checkPhoneExists(String phone) {
        LambdaQueryWrapper<User> wrapper =
                Wrappers.lambdaQuery(User.class).eq(User::getPhone, phone);
        return baseMapper.selectCount(wrapper);
    }

    /**
     * 保存用户信息、用户详情信息；
     */
    private void saveUserInfo(UserRegistDto userRegistDto) {
        // 保存用户信息; 密码加密：SpringSecurity提供的加密模块
        User user = new User();
        user.setPhone(userRegistDto.getPhone());
        user.setUserName(userRegistDto.getUserName());
        user.setPwd(passwordEncoder.encode(userRegistDto.getPwd()));
        user.setUserType(UserConstant.USER_TYPE_PERSONAL);
        user.setUserStatus(true);
        this.save(user);

        // 保存用户详情信息；
        UserDetail userDetail = new UserDetail();
        userDetail.setId(user.getId());
        userDetail.setUserName(user.getUserName());
        userDetail.setPhone(user.getPhone());
        userDetail.setBorrowMoney(new BigDecimal("0"));
        userDetail.setUserCode(UniqueIDGenerator.generator(UserConstant.USER_CODE_PREFIX));
        userDetail.setCardBindStatus(false);
        userDetailService.save(userDetail);
    }


    /**
     * isPhoneLogin
     *   1、true 表示根据手机号码、验证码 登陆
     *      1.1 先访问《发送验证码接口》，发送短信验证码
     *      1.2 校验验证码: loginSecret 与 Redis中验证码是否一致
     *      1.3 如果验证码输入正确，再查询手机号码在数据库是否存在
     *          不存在：自动注册，保存用户、用户明细
     *              用户名：随机生成
     *              密码：默认123456
     *              对于用户名与密码，登陆后可以修改
     *          存在：返回
     *   2、false 表示根据用户名密码登陆
     *      校验用户名密码是否存在即可
     */
    @Override
    public LoginVo login(LoginDto loginDto) {
        // 获取登录方式: false 表示用户名密码登录
        Boolean isPhoneLogin = loginDto.getIsPhoneLogin();
        if (!isPhoneLogin) {
            return upLogin(loginDto.getLoginName(),loginDto.getLoginSecret());
        } else {
            return smsLogin(loginDto.getLoginName(),loginDto.getLoginSecret());
        }
    }

    /**
     * 根据手机号、验证码登录；如果手机号码不存在，自动注册（简化操作）
     * @param phone 手机号
     * @param code 验证码
     * @return
     */
    private LoginVo smsLogin(String phone, String code) {
        //1. 获取Redis中的验证码
        String smsKey = RedisConstant.SMS_PREFIX + phone;
        String redisCode = stringRedisTemplate.opsForValue().get(smsKey);
        //2. 校验验证码
        Assert.error(
                code!=null && !code.equals(redisCode),
                BusinessCode.USER_VERIFY_CODE_ERROR);

        //3. 删除验证码
        stringRedisTemplate.delete(smsKey);

        //4、根据手机号码查询
        User user = findByPhone(phone);

        //5、判断手机号码是否存在，不存在自动注册
        if (user == null) {
            UserRegistDto userRegistDto = new UserRegistDto();
            userRegistDto.setPhone(phone);
            userRegistDto.setPwd("123456");
            userRegistDto.setUserName(RandomUtil.randomString(6));
            // 自动注册: 保存用户、用户明细
            this.saveUserInfo(userRegistDto);

            // 解决：如果手机号码不存在，执行if保存用户，保存后user对象还是为null；所以这里重新查询
            user = findByPhone(phone);
        }
        // 返回
        LoginVo loginVo = getLoginVo(user);
        return loginVo;
    }

    private User findByPhone(String phone) {
        return this.getOne(Wrappers.lambdaQuery(User.class).eq(User::getPhone,phone));
    }

    /**
     * 用户名密码登录
     * @param userName
     * @param password
     */
    private LoginVo upLogin(String userName, String password) {
        // 根据用户名查询
        User user = this.getOne(Wrappers.lambdaQuery(User.class).eq(User::getUserName, userName));

        // 判断用户名是否存在
        Assert.error(user == null, BusinessCode.USER_LOGIN_ERROR);

        // 判断密码
        Assert.error(
                !passwordEncoder.matches(password,user.getPwd()),
                BusinessCode.USER_LOGIN_ERROR);

        // 返回
        LoginVo loginVo = getLoginVo(user);
        return loginVo;
    }

    /**
     * 登录返回结果统一封装：无论是账号密码登录、手机号验证码登录，都在这里统一返回
     * @param user
     * @return
     */
    private LoginVo getLoginVo(User user) {
        LoginVo loginVo = BeanUtil.copyProperties(user,LoginVo.class);

        // 准备token数据 (通过Hutool工具类创建一个HashMap对象)
        Map<String, Object> payload = MapUtil.<String, Object>builder()
                .put(UserConstant.TOKEN_USERID, user.getId().toString())
                .put(UserConstant.TOKEN_PHONE, user.getPhone()).build();

        // 生成token
        String token = jwtTemplate.createToken(payload);
        // 设置返回token
        loginVo.setToken(token);

        // token 存储到Redis中，通过设置Redis的有效时间实现Token续期、管理用户登录状态。
        stringRedisTemplate.opsForValue().set(
                UserConstant.USER_TOKEN+user.getId(),token, Duration.ofMinutes(30));

        return loginVo;
    }


    /**
     * 绑卡业务流程：
     * 1、获取token中登录用户
     * 2、查询用户表，判断当前用户是否已经绑卡
     * 3、判断提交过来的银行卡是否已被绑定
     * 4、如卡未被绑定，保存用户绑卡信息
     * 5、更新用户的信息、用户明细
     * 6、远程访问托管代理，通过托管代理访问银行托管。
     */
    @Override
    public SignatureDTO createConsumer(CardBindDTO cardBindDTO) {
        //1、获取token中登录用户
        TokenVo tokenVo = TokenUtils.getTokenVo();

        //2、查询用户表，判断当前用户是否已经绑卡: u_user_detail中CARD_BIND_STATUS字段
        UserDetail userDetail = userDetailService.getById(tokenVo.getUserId());
        // CARD_BIND_STATUS = 1 说明已经绑定了银行卡，返回对应提示
        Assert.error(userDetail.getCardBindStatus(),BusinessCode.USER_CARD_ALREADY_BINDED);

        //3、判断提交过来的银行卡是否已被绑定,在绑定记录表中是否存在对应记录: u_user_card
        UserCard userCard = userCardService.getOne(
                Wrappers.lambdaQuery(UserCard.class).eq(UserCard::getCardNo, cardBindDTO.getCardNo()));
        Assert.error(userCard != null, BusinessCode.USER_CARD_ALREADY_BINDED);

        //4、如卡未被绑定，保存用户绑卡信息: u_user_card
        UserCard dbCard = new UserCard();
        dbCard.setUserId(Long.parseLong(tokenVo.getUserId()));
        dbCard.setPhone(tokenVo.getPhone());
        dbCard.setCardNo(cardBindDTO.getCardNo());
        dbCard.setStatus(true);
        userCardService.save(dbCard);

        //5、更新用户的信息、用户明细
        //5.1、根据请求参数realName、identityCode，修改用户表u_user
        this.update(Wrappers.lambdaUpdate(User.class)
                .set(User::getRealName,cardBindDTO.getRealName())
                .set(User::getIdentityCode,cardBindDTO.getIdentityCode())
                .eq(User::getId,tokenVo.getUserId())
        );
        //5.2、修改用户明细：reqCode 请求流水号，自动生成，唯一
        String reqCode = UniqueIDGenerator.generator(UserConstant.REQ_CODE_PREFIX);
        userDetailService.update(Wrappers.lambdaUpdate(UserDetail.class)
                .set(UserDetail::getReqCode,reqCode)
                .eq(UserDetail::getId,tokenVo.getUserId())
        );

        //6、远程访问托管代理，通过托管代理访问银行托管。


        // 请求数据
        BankCardAddDTO bankCardAddDTO = new BankCardAddDTO();
        bankCardAddDTO.setReqCode(reqCode);
        bankCardAddDTO.setUserId(dbCard.getUserId());
        /*// 访问托管系统的地址
        String url = "http://localhost:5500/bank/access/card";
        // 通过restTemplate远程调用
        ResponseEntity<SignatureDTO> entity =
                restTemplate.postForEntity(url, bankCardAddDTO, SignatureDTO.class);
        SignatureDTO signatureDTO = entity.getBody();
        return signatureDTO;*/

        // 通过Openfeign实现远程调用 (像调用本地方法一样，进行远程调用)
        SignatureDTO signatureDTO = tradeLogApi.proxyBindCardLog(bankCardAddDTO);
        return signatureDTO;
    }

    @Override
    public UserInfoVo findById(Long userId) {
        // 返回字段分析：user表（userName,identityCode）
        // 返回字段分析：user_detail表（userCode）

        // 根据id查询数据库
        User user = this.getById(userId);
        // 自定义断言判断，减少if判断，让代码更简洁
        Assert.error(user == null,ResultCode.DATA_IS_WRONG);
        // 返回结果
        UserInfoVo userInfoVo = new UserInfoVo();
        userInfoVo.setUserName(user.getUserName());
        userInfoVo.setIdentityCode(user.getIdentityCode());

        // 根据用户id查询用户编码； 用户与用户详情共用主键
        UserDetail userDetail = userDetailService.getById(userId);
        userInfoVo.setUserCode(userDetail.getUserCode());

        return userInfoVo;
    }

}
